/****************************************************************************
*  Arthea Server by R. Jennings (2007-2008)   http://arthea.googlecode.com/ *
*  By using this code you comply with the Artistic and GPLv2 Licenses.      *
****************************************************************************/


using System;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Arthea.Connections
{
    /// <summary>
    /// Telnet options
    /// </summary>
    public struct Telopt
    {
        /// <summary>
        /// backspace
        /// </summary>
        public const byte Backspace = 8;

        /// <summary>
        /// MCCP v1
        /// </summary>
        public const byte Compress = 85;

        /// <summary>
        /// MCCP v2
        /// </summary>
        public const byte Compress2 = 86;

        /// <summary>
        /// server does
        /// </summary>
        public const byte Do = 253;

        /// <summary>
        /// server doesn't
        /// </summary>
        public const byte Dont = 254;

        /// <summary>
        /// echo
        /// </summary>
        public const byte Echo = 1;

        /// <summary>
        /// End of Record
        /// </summary>
        public const byte EOR = 25;

        /// <summary>
        /// Go Ahead
        /// </summary>
        public const byte GA = 3;

        /// <summary>
        /// iac
        /// </summary>
        public const byte IAC = 255;

        /// <summary>
        /// negotiate about window size
        /// </summary>
        public const byte NAWS = 31;

        /// <summary>
        /// Mud eXtension Protocol
        /// </summary>
        public const byte MXP = 91;

        /// <summary>
        /// Mud Sound Protocol
        /// </summary>
        public const byte MSP = 90;

        /// <summary>
        /// new-environment
        /// </summary>
        public const byte NE = 39;

        /// <summary>
        /// no operation
        /// </summary>
        public const byte NOP = 241;

        /// <summary>
        /// sub negotiations
        /// </summary>
        public const byte SB = 250;

        /// <summary>
        /// signal end of sub negotiations
        /// </summary>
        public const byte SE = 240;

        /// <summary>
        /// send option
        /// </summary>
        public const byte Send = 1;

        /// <summary>
        /// terminal type
        /// </summary>
        public const byte TerminalType = 24;

        /// <summary>
        /// Value for NE.
        /// </summary>
        public const byte Value = 1;

        /// <summary>
        /// Variable for NE.
        /// </summary>
        public const byte Var = 0;

        /// <summary>
        /// client will
        /// </summary>
        public const byte Will = 251;

        /// <summary>
        /// client wont
        /// </summary>
        public const byte Wont = 252;
    }

    /// <summary>
    /// Implementation of telnet.
    /// </summary>
    public struct Telnet
    {
        /// <summary>
        /// Turn on MCCP v2
        /// </summary>
        public static readonly byte[] Compress2Start =
            new byte[] {Telopt.IAC, Telopt.SB, Telopt.Compress2, Telopt.IAC, Telopt.SE};

        /// <summary>
        /// Turn on MCCP v1
        /// </summary>
        public static readonly byte[] CompressStart =
            new byte[] {Telopt.IAC, Telopt.SB, Telopt.Compress, Telopt.Will, Telopt.SE};

        /// <summary>
        /// Turns echo off for the user
        /// </summary>
        public static readonly byte[] EchoOff = new byte[] {Telopt.IAC, Telopt.Will, Telopt.Echo};

        /// <summary>
        /// Turns echo on for the user
        /// </summary>
        public static readonly byte[] EchoOn = new byte[] {Telopt.IAC, Telopt.Wont, Telopt.Echo};

        /// <summary>
        /// Go Ahead
        /// </summary>
        public static readonly byte[] GA = new byte[] {Telopt.IAC, Telopt.GA};

        private static readonly byte[] RequestTType =
            new byte[] {Telopt.IAC, Telopt.SB, Telopt.TerminalType, Telopt.Send, Telopt.IAC, Telopt.SE};


        private static byte[] RequestEnvVars()
        {
            MemoryStream bytes = new MemoryStream();

            bytes.WriteByte(Telopt.IAC);
            bytes.WriteByte(Telopt.SB);
            bytes.WriteByte(Telopt.NE);
            bytes.WriteByte(Telopt.Send);
            bytes.WriteByte(Telopt.Var);

            foreach (byte c in ServerConfig.Encoding.GetBytes("\"SYSTEMTYPE\""))
                bytes.WriteByte(c);

            bytes.WriteByte(Telopt.IAC);
            bytes.WriteByte(Telopt.SE);

            return bytes.ToArray();
        }

        /// <summary>
        /// Processes any telnet options.
        /// </summary>
        /// <param name="bytes">The bytes.</param>
        /// <param name="index">The index.</param>
        /// <param name="con">The connection.</param>
        public static void Process(byte[] bytes, ref int index, Connection con)
        {
            Debug.Assert(bytes[index] == Telopt.IAC);

            index++;

            switch (bytes[index])
            {
                case Telopt.Will:
                    switch (bytes[++index])
                    {
                        case Telopt.MXP:
                            // read defined elements
                            con.Flags.Add(ConnectionFlags.MXP);
                            con.TeloptDo(Telopt.MXP);
                            break;
                        case Telopt.MSP:
                            // send start sound
                            con.Flags.Add(ConnectionFlags.MSP);
                            con.TeloptDo(Telopt.MSP);
                            break;
                        case Telopt.Compress:
                        case Telopt.Compress2:
#if ENABLE_COMPRESSION
                            con.TeloptDo(bytes[index]);
#else
                            con.TeloptDont(bytes[index]);
#endif
                            break;
                        case Telopt.NAWS:
                            con.TeloptDo(bytes[index]);
                            break;
                        case Telopt.TerminalType:
                            con.WriteToSocket(RequestTType);
                            break;
                        case Telopt.NE:
                            con.WriteToSocket(RequestEnvVars());
                            break;
                        case Telopt.GA:
                            con.Flags.Add(ConnectionFlags.GA);
                            break;
                        case Telopt.EOR:
                            con.Flags.Add(ConnectionFlags.EOR);
                            break;
                        default:
                            Log.Error("unknown telopt will {0}", bytes[index]);
                            break;
                    }
                    break;
                case Telopt.Wont:
                    switch (bytes[++index])
                    {
                        case Telopt.MXP:
                            con.Flags.Remove(ConnectionFlags.MXP);
                            break;
                        case Telopt.MSP:
                            con.Flags.Remove(ConnectionFlags.MSP);
                            break;
                        case Telopt.GA:
                            con.Flags.Remove(ConnectionFlags.GA);
                            break;
                        case Telopt.EOR:
                            con.Flags.Remove(ConnectionFlags.EOR);
                            break;
                        default:
                            Log.Error("unknown telopt won't {0}", bytes[index]);
                            break;
                    }
                    break;
                case Telopt.Do:
                    switch (bytes[++index])
                    {
                        case Telopt.GA:
                            con.Flags.Add(ConnectionFlags.GA);
                            break;
                        case Telopt.EOR:
                            con.Flags.Add(ConnectionFlags.EOR);
                            break;
                        case Telopt.MXP:
                            con.Flags.Add(ConnectionFlags.MXP);
                            break;
                        case Telopt.MSP:
                            con.Flags.Add(ConnectionFlags.MSP);
                            break;
                        case Telopt.TerminalType:
                            con.WriteToSocket(RequestTType);
                            break;

                        case Telopt.Compress:
                            if (con.Flags.Has(ConnectionFlags.MCCP))
                            {
                                con.TeloptWont(Telopt.Compress);
                            }
                            else
                            {
#if ENABLE_COMPRESSION
                                con.WriteToSocket(CompressStart);
                                con.StartCompression();
#endif
                            }
                            break;
                        case Telopt.Compress2:
                            if (con.Flags.Has(ConnectionFlags.MCCP))
                            {
                                con.TeloptWont(Telopt.Compress2);
                            }
                            else
                            {
#if ENABLE_COMPRESSION
                                con.WriteToSocket(Compress2Start);
                                con.StartCompression();
#endif
                            }
                            break;
                        default:
                            Log.Error("unknown telopt do {0}", bytes[index]);
                            break;
                    }
                    break;
                case Telopt.Dont:
                    switch (bytes[++index])
                    {
                        case Telopt.Compress:
                        case Telopt.Compress2:
                            break;
                        case Telopt.GA:
                            con.Flags.Remove(ConnectionFlags.GA);
                            break;
                        case Telopt.EOR:
                            con.Flags.Remove(ConnectionFlags.EOR);
                            break;
                        default:
                            break;
                    }
                    break;
                case Telopt.SE:
                    break;
                case Telopt.SB:
                    switch (bytes[++index])
                    {
                        case Telopt.NAWS:
                            ushort[] vars = new ushort[4];

                            for (int i = 0; i < vars.Length; i++)
                            {
                                vars[i] = bytes[++index];
                                if (bytes[index] == Telopt.IAC)
                                    ++index;
                                if ((i == 0 || i == 2) && vars[i] > 0)
                                    vars[i] += Telopt.IAC;
                            }

                            con.ScreenWidth = (ushort) (vars[0] + vars[1]);
                            con.ScreenHeight = (ushort) (vars[2] + vars[3]);
                            con.Flags.Add(ConnectionFlags.NAWS);
                            break;
                        case Telopt.TerminalType:
                            StringBuilder buf = new StringBuilder();

                            index += 2;
                            while (bytes[index] != Telopt.IAC)
                            {
                                buf.Append(Convert.ToChar(bytes[index++]));
                            }
                            con.TerminalType = buf.ToString();
                            con.Flags.Add(ConnectionFlags.TType);
                            ++index;
                            break;
                        case Telopt.NE:
                            index += 2;
                            while (bytes[index] != Telopt.IAC)
                            {
                                StringBuilder var = new StringBuilder();

                                ++index;

                                while (bytes[index] != Telopt.Value && bytes[index] != Telopt.IAC)
                                {
                                    var.Append(Convert.ToChar(bytes[index++]));
                                }

                                if (bytes[index] == Telopt.IAC)
                                    break;

                                ++index;

                                StringBuilder value = new StringBuilder();

                                while (bytes[index] != Telopt.Var && bytes[index] != Telopt.IAC)
                                {
                                    value.Append(Convert.ToChar(bytes[index++]));
                                }

                                if (var.ToString().ToLower() == "systemtype")
                                    con.SystemType = value.ToString();
                            }
                            con.Flags.Add(ConnectionFlags.NE);
                            ++index; // SE
                            break;
                        default:
                            Log.Bug("unknown telnet sub negotiation {0}.", bytes[index]);
                            break;
                    }
                    break;
                default:
                    Log.Bug("Unknown telnet option IAC {0} {1}", bytes[index++], bytes[index]);
                    break;
            }
        }
    }
}